/*
 * Decimal.java
 *
 *  
 */
package org.msb.finance.util;

import java.text.NumberFormat;

/**
 * 
 * 
 * @author Marc Boudreau
 * 
 */
public class Decimal {

	/*
	 * The scale used to convert the internal value into the actual value.
	 */
	private static final int SCALE = 10000;

	/*
	 * 
	 */
	private static final NumberFormat defaultFormatter = NumberFormat.getNumberInstance();

	/*
	 * The internal value of this object. By using an integer value rounding and precision errors can be avoided.
	 */
	private int value;

	/*
	 * A lazily constructed String representation of this instance. It is constructed in the toString() method, but any
	 * of the methods that can alter the internal value must reset this field to null.
	 */
	private String cachedToString;

	/*
	 * The NumberFormat object used to format this instance into a String object with the correct number of decimal
	 * places.
	 */
	private NumberFormat formatter = defaultFormatter;

	/*
	 * Initializes the formatting settings for the default NumberFormat object.
	 */
	static {
		defaultFormatter.setMaximumFractionDigits(2);
		defaultFormatter.setMinimumFractionDigits(2);
		defaultFormatter.setMinimumIntegerDigits(1);
		defaultFormatter.setGroupingUsed(false);
	}

	/**
	 * Constructs a new instance with the default value of zero (0).
	 */
	public Decimal() {
		this(0);
	}

	/**
	 * Constructs a new instance that has the same value as the provided {@code Decimal} object.
	 * 
	 * @param source
	 *            The {@code Decimal} object whose value will be copied into the new instance.
	 */
	public Decimal(Decimal source) {
		this(source.value);
	}

	/**
	 * 
	 * @param source
	 * @param formatter
	 */
	public Decimal(Decimal source, NumberFormat formatter) {
		this(source.value, formatter);
	}

	/**
	 * Constructs a new instance with the provided value. The provided {@code int} value is expected to be 10,000 times
	 * the actual value being represented by the new instance. As such the 4 least significant digits represent the 4
	 * most significant fraction digits of the represented number.
	 * 
	 * @param value
	 *            An {@code int} value that is scaled up by 10,000.
	 */
	public Decimal(int value) {
		this(value, defaultFormatter);
	}

	/**
	 * 
	 * @param value
	 * @param formatter
	 */
	public Decimal(int value, NumberFormat formatter) {
		this.value = value;
		this.formatter = formatter;
	}

	/**
	 * Returns this instance's {@link NumberFormat} object used to render a text representation of its value.
	 * 
	 * @return A reference to this instance's {@link NumberFormat} object.
	 */
	public NumberFormat getNumberFormat() {
		return this.formatter;
	}

	/**
	 * Sets this instance's {@link NumberFormat} object to the provided instance.
	 * 
	 * @param formatter
	 *            The {@link NumberFormat} object to be used by this instance.
	 */
	public void setNumberFormat(NumberFormat formatter) {
		this.formatter = formatter;

		resetCachedToString();
	}

	/**
	 * Adds the provided {@code Decimal} object's value to this instance's value. The result is then assigned to this
	 * instance; much like the {@code +=} operator for {@code int} types.
	 * 
	 * @param decimal
	 *            The {@code Decimal} instance to add to this instance.
	 * 
	 * @return A reference to this instance.
	 */
	public Decimal add(Decimal decimal) {
		this.value += decimal.value;

		if (0 != decimal.value) {
			resetCachedToString();
		}

		return this;
	}

	/**
	 * Adds the provided {@code int} value to this instance's value. The result is then assigned to this instance; much
	 * like the {@code +=} operator for {@code int} types.
	 * 
	 * @param value
	 *            The {@code int} value to add to this instance.
	 * 
	 * @return A reference to this instance.
	 */
	public Decimal add(int value) {
		this.value += (value * SCALE);

		if (0 != value) {
			resetCachedToString();
		}

		return this;
	}

	/**
	 * Adds the provided {@code float} value to this instance's value. The result is then assigned to this instance;
	 * much like the {@code +=} operator for {@code int} types.
	 * 
	 * @param value
	 *            The {@code float} value to add to this instance.
	 * 
	 * @return A reference to this instance.
	 */
	public Decimal add(float value) {
		this.value += Math.round(value * SCALE);

		if (0f != value) {
			resetCachedToString();
		}

		return this;
	}

	/**
	 * Adds the provided {@link Number} object's value to this instance's value. The result is then assigned to this
	 * instance; much like the {@code +=} operator for {@code int} types.
	 * 
	 * @param number
	 *            The {@link Number} object to add to this instance.
	 * 
	 * @return A reference to this instance.
	 */
	public Decimal add(Number number) {
		return add(number.floatValue());
	}

	/**
	 * Subtracts the provided {@code Decimal} object's value from this instance's value. The result is then assigned to
	 * this instance; much like the {@code -=} operator for {@code int} types.
	 * 
	 * @param decimal
	 *            The {@code Decimal} object to subtract from this instance.
	 * 
	 * @return A reference to this instance.
	 */
	public Decimal subtract(Decimal decimal) {
		this.value -= decimal.value;

		if (0 != decimal.value) {
			resetCachedToString();
		}

		return this;
	}

	/**
	 * Subtracts the provided {@code int} value from this instance's value. The result is then assigned to this
	 * instance; much like the {@code -=} operator for {@code int} types.
	 * 
	 * @param value
	 *            The {@code int} value to subtract from this instance.
	 * 
	 * @return A reference to this instance.
	 */
	public Decimal subtract(int value) {
		this.value -= (value * SCALE);

		if (0 != value) {
			resetCachedToString();
		}

		return this;
	}

	/**
	 * Subtracts the provided {@code float} value from this instance's value. The result is then assigned to this
	 * instance; much like the {@code -=} operator for {@code int} types.
	 * 
	 * @param value
	 *            The {@code float} value to subtract from this instance.
	 * 
	 * @return A reference to this instance.
	 */
	public Decimal subtract(float value) {
		this.value -= Math.round(value * SCALE);

		if (0f != value) {
			resetCachedToString();
		}

		return this;
	}

	/**
	 * Subtracts the provided {@link Number} object's value from this instance's value. The result is then assigned to
	 * this instance; much like the {@code -=} operator for {@code int} types.
	 * 
	 * @param number
	 *            The {@link Number} object to subtract from this instance.
	 * 
	 * @return A reference to this instance.
	 */
	public Decimal subtract(Number number) {
		return subtract(number.floatValue());
	}

	/**
	 * Multiplies the provided {@code Decimal} object's value by the this instance's value. The result is then assigned
	 * to this instance; much like the {@code *=} operator for {@code int} types.
	 * 
	 * @param decimal
	 *            The {@code Decimal} object by which this instance is multiplied.
	 * 
	 * @return A reference to this instance.
	 */
	public Decimal multiply(Decimal decimal) {
		long l = (long) this.value * (long) decimal.value;
		this.value = (int) (l / SCALE);

		if (10000 != decimal.value) {
			resetCachedToString();
		}

		return this;
	}

	/**
	 * Multiplies the provided {@code int} value by the this instance's value. The result is then assigned to this
	 * instance; much like the {@code *=} operator for {@code int} types.
	 * 
	 * @param value
	 *            The {@code int} value by which this instance is multiplied.
	 * 
	 * @return A reference to this instance.
	 */
	public Decimal multiply(int value) {
		this.value *= value;

		if (1 != value) {
			resetCachedToString();
		}

		return this;
	}

	/**
	 * Multiplies the provided {@code float} value by the this instance's value. The result is then assigned to this
	 * instance; much like the {@code *=} operator for {@code int} types.
	 * 
	 * @param value
	 *            The {@code float} value by which this instance is multiplied.
	 * 
	 * @return A reference to this instance.
	 */
	public Decimal multiply(float value) {
		this.value = Math.round(this.value * value);

		if (1f != value) {
			resetCachedToString();
		}

		return this;
	}

	/**
	 * Multiplies the provided {@link Number} object's value by the this instance's value. The result is then assigned
	 * to this instance; much like the {@code *=} operator for {@code int} types.
	 * 
	 * @param number
	 *            The {@link Number} object by which this instance is multiplied.
	 * 
	 * @return A reference to this instance.
	 */
	public Decimal multiply(Number number) {
		return multiply(number.floatValue());
	}

	/**
	 * Divides this instance's value by the provided {@code Decimal} object's value. The result is then assigned to this
	 * instance; much like the {@code /=} operator for {@code int} types.
	 * 
	 * @param decimal
	 *            The {@code Decimal} object by which this instance is divided.
	 * 
	 * @return A reference to this instance.
	 */
	@SuppressWarnings("nls")
	public Decimal divide(Decimal decimal) {
		if (0 == decimal.value) {
			throw new ArithmeticException("Division by zero.");
		}

		double dbl = (double) this.value / (decimal.value);

		this.value = (int) Math.round(dbl * SCALE);

		if (10000 != decimal.value) {
			resetCachedToString();
		}

		return this;
	}

	/**
	 * Divides this instance's value by the provided {@code int} value. The result is then assigned to this instance;
	 * much like the {@code /=} operator for {@code int} types.
	 * 
	 * @param value
	 *            The {@code int} value by which this instance is divided.
	 * 
	 * @return A reference to this instance.
	 */
	public Decimal divide(int value) {
		this.value /= value;

		if (1 != value) {
			resetCachedToString();
		}

		return this;
	}

	/**
	 * Divides this instance's value by the provided {@code float} value. The result is then assigned to this instance;
	 * much like the {@code /=} operator for {@code int} types.
	 * 
	 * @param value
	 *            The {@code float} value by which this instance is divided.
	 * 
	 * @return A reference to this instance.
	 */
	@SuppressWarnings("nls")
	public Decimal divide(float value) {
		if (0f == value) {
			throw new ArithmeticException("Division by zero.");
		}

		this.value /= value;

		if (1f != value) {
			resetCachedToString();
		}

		return this;
	}

	/**
	 * Divides this instance's value by the provided {@link Number} object's value. The result is then assigned to this
	 * instance; much like the {@code /=} operator for {@code int} types.
	 * 
	 * @param number
	 *            The {@link Number} object by which this instance is divided.
	 * 
	 * @return A reference to this instance.
	 */
	public Decimal divide(Number number) {
		return divide(number.floatValue());
	}

	/*
	 * Resets the cached String object containing the text representation of this instance's value. This method is
	 * called whenever the internal value is changed.
	 */
	private void resetCachedToString() {
		this.cachedToString = null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		if (null == this.cachedToString) {
			this.cachedToString = this.formatter.format((double) this.value / SCALE);
		}

		return this.cachedToString;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object object) {
		if (this == object) {
			return true;
		} else if (null != object && object.getClass() == Decimal.class) {
			return this.value == ((Decimal) object).value;
		}

		return false;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		return this.value;
	}

	/**
	 * Converts this instance's value to a floating-point type. This may incur a loss of precision.
	 * 
	 * @return This instance's value converted to a {@code float} value.
	 */
	public float toFloat() {
		return (float) this.value / SCALE;
	}

	/**
	 * Returns a text representation of this instance's value using the provided {@link NumberFormat} object instead of
	 * the built-in one. Use this method when a different formatting settings are required for a given function rather
	 * than changing the instance's {@link NumberFormat} object with the {@link #setNumberFormat(NumberFormat)} method.
	 * 
	 * @param formatter
	 *            The {@link NumberFormat} object to use to render this instance's value.
	 * 
	 * @return A {@link String} object containing the text representation of this instance's value.
	 */
	public String format(NumberFormat formatter) {
		return formatter.format((double) this.value / SCALE);
	}
}
